1use self::cargo_fuzz::CargoFuzzCommand;
5use self::parse_fuzz_crate_toml::RepoFuzzTarget;
6use crate::Xtask;
7use anyhow::Context;
8use clap::Parser;
9use serde::Deserialize;
10use std::collections::BTreeMap;
11use std::io::Write;
12use std::path::PathBuf;
13
14mod cargo_fuzz;
15mod html_coverage;
16mod init_from_template;
17mod onefuzz_schema;
18mod parse_fuzz_crate_toml;
19
20#[derive(Parser)]
22#[clap(
23 about = "Superset of `cargo fuzz` features, tailored to the HvLite repo",
24 disable_help_subcommand = true
25)]
26#[clap(after_help = r#"ADDITIONAL NOTES:
27
28 Fuzzers in the HvLite repo are required to include a
29 [package.metadata.xtask.fuzz.onefuzz-allowlist] section in their Cargo.toml.
30
31 Allowlists are used by OneFuzz to limit which files are considered when
32 generating code coverage reports.
33
34 A typical declaration might something like:
35
36 [package.metadata.xtask.fuzz.onefuzz-allowlist]
37 fuzz_my_crate = [
38 "fuzz_my_crate.rs"
39 "../src/**/*",
40 "!../src/vendored/**/*"
41 ]
42
43 In this example, code coverage reports would consider the fuzzer itself
44 and all files under the crate's `src/` directory, excluding code under
45 `src/vendored/`.
46
47 NOTE: Omitting this table will result in verification failures!
48"#)]
49pub struct Fuzz {
50 #[clap(subcommand)]
52 cmd: FuzzCommand,
53}
54
55#[derive(clap::Subcommand)]
56enum FuzzCommand {
57 Init {
59 package: String,
61
62 template: init_from_template::Template,
64 },
65 List {
67 #[clap(long)]
75 crates: bool,
76 },
77 Verify,
80 Build {
82 targets: Vec<String>,
85
86 #[clap(long)]
88 toolchain: Option<String>,
89
90 #[clap(raw(true))]
92 extra: Vec<String>,
93 },
94 Run {
96 target: String,
98
99 artifact: Option<PathBuf>,
101
102 #[clap(long)]
104 toolchain: Option<String>,
105
106 #[clap(raw(true))]
108 extra: Vec<String>,
109 },
110 Clean {
112 targets: Vec<String>,
115
116 #[clap(long)]
118 keep_corpus: bool,
119
120 #[clap(long)]
122 keep_artifacts: bool,
123
124 #[clap(long)]
126 keep_coverage: bool,
127 },
128 Fmt {
130 target: String,
132
133 input: PathBuf,
135
136 #[clap(long)]
138 toolchain: Option<String>,
139
140 #[clap(raw(true))]
142 extra: Vec<String>,
143 },
144 Cmin {
146 target: String,
148
149 #[clap(long)]
151 toolchain: Option<String>,
152
153 #[clap(raw(true))]
155 extra: Vec<String>,
156 },
157 Tmin {
159 target: String,
161
162 test_case: PathBuf,
164
165 #[clap(long)]
167 toolchain: Option<String>,
168
169 #[clap(raw(true))]
171 extra: Vec<String>,
172 },
173 Coverage {
175 target: String,
177
178 #[clap(long)]
180 with_html_report: bool,
181
182 #[clap(long, requires = "with_html_report")]
184 only_report: bool,
185
186 #[clap(long)]
188 toolchain: Option<String>,
189
190 #[clap(raw(true))]
192 extra: Vec<String>,
193 },
194 Onefuzz {
196 config_path: PathBuf,
198
199 out_dir: PathBuf,
201
202 target: Vec<String>,
205
206 #[clap(long)]
208 toolchain: Option<String>,
209 },
210 Dump,
212}
213
214mod cargo_package_metadata {
215 use serde::Deserialize;
216 use serde::Serialize;
217 use std::collections::BTreeMap;
218
219 #[derive(Serialize, Deserialize)]
220 pub struct PackageMetadata {
221 #[serde(rename = "cargo-fuzz")]
222 pub cargo_fuzz: Option<bool>, pub xtask: Option<Xtask>,
224 }
225
226 #[derive(Serialize, Deserialize)]
227 pub struct Xtask {
228 pub fuzz: Option<Fuzz>,
229 }
230
231 #[derive(Serialize, Deserialize)]
232 pub struct Fuzz {
233 #[serde(rename = "onefuzz-allowlist")]
234 pub allowlist: BTreeMap<String, Vec<String>>,
235
236 #[serde(default, rename = "target-options")]
237 pub target_options: BTreeMap<String, Vec<String>>,
238 }
239}
240
241impl Xtask for Fuzz {
242 fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
243 let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(&ctx)?;
244 let fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
245
246 match self.cmd {
247 FuzzCommand::Init { package, template } => {
248 init_from_template::init_from_template(&ctx, package, template)?;
249 }
250 FuzzCommand::Dump => {
251 println!("{:#?}", fuzz_targets)
252 }
253 FuzzCommand::Verify => {
254 log::info!("fuzzing crates were successfully verified!")
257 }
258 FuzzCommand::List { crates } => {
259 if crates {
260 for parse_fuzz_crate_toml::FuzzCrateMetadata { crate_name, .. } in fuzz_crates {
261 println!("{}", crate_name)
262 }
263 } else {
264 for (name, _meta) in fuzz_targets {
265 println!("{}", name)
266 }
267 }
268 }
269 FuzzCommand::Build {
270 targets,
271 toolchain,
272 extra,
273 } => {
274 let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, targets)?;
275
276 for (name, meta) in selected_fuzz_targets {
277 println!("building '{}'", name);
278 CargoFuzzCommand::Build.invoke(
279 &name,
280 &meta.fuzz_dir,
281 &meta.target_options,
282 toolchain.as_deref(),
283 &extra,
284 )?;
285 }
286 }
287 FuzzCommand::Run {
288 target: target_name,
289 artifact,
290 toolchain,
291 extra,
292 } => {
293 let target = select_fuzz_target(fuzz_targets, &target_name)?;
294 let res = CargoFuzzCommand::Run { artifact }.invoke(
295 &target_name,
296 &target.fuzz_dir,
297 &target.target_options,
298 toolchain.as_deref(),
299 &extra,
300 );
301
302 if let Err(e) = res {
303 log::warn!(
304 "Reminder: Make sure you swap `cargo fuzz` with `cargo xtask fuzz` when repro-ing / minimizing failures in the HvLite repo!"
305 );
306 return Err(e);
307 }
308 }
309 FuzzCommand::Fmt {
310 target: target_name,
311 input,
312 extra,
313 toolchain,
314 } => {
315 let target = select_fuzz_target(fuzz_targets, &target_name)?;
316
317 CargoFuzzCommand::Fmt { input }.invoke(
318 &target_name,
319 &target.fuzz_dir,
320 &target.target_options,
321 toolchain.as_deref(),
322 &extra,
323 )?;
324 }
325 FuzzCommand::Cmin {
326 target: target_name,
327 extra,
328 toolchain,
329 } => {
330 let target = select_fuzz_target(fuzz_targets, &target_name)?;
331
332 CargoFuzzCommand::Cmin.invoke(
333 &target_name,
334 &target.fuzz_dir,
335 &target.target_options,
336 toolchain.as_deref(),
337 &extra,
338 )?;
339 }
340 FuzzCommand::Tmin {
341 target: target_name,
342 test_case,
343 toolchain,
344 extra,
345 } => {
346 let target = select_fuzz_target(fuzz_targets, &target_name)?;
347 let res = CargoFuzzCommand::Tmin { test_case }.invoke(
348 &target_name,
349 &target.fuzz_dir,
350 &target.target_options,
351 toolchain.as_deref(),
352 &extra,
353 );
354
355 if let Err(e) = res {
356 log::warn!(
357 "Reminder: Make sure you swap `cargo fuzz` with `cargo xtask fuzz` when repro-ing / minimizing failures in the HvLite repo!"
358 );
359 return Err(e);
360 }
361 }
362 FuzzCommand::Coverage {
363 target: target_name,
364 with_html_report,
365 only_report,
366 toolchain,
367 extra,
368 } => {
369 let target = select_fuzz_target(fuzz_targets, &target_name)?;
370
371 if !only_report {
372 CargoFuzzCommand::Coverage.invoke(
373 &target_name,
374 &target.fuzz_dir,
375 &target.target_options,
376 toolchain.as_deref(),
377 &extra,
378 )?;
379 }
380
381 if with_html_report {
382 html_coverage::generate_html_coverage_report(
383 &ctx,
384 &target.fuzz_dir,
385 &target_name,
386 )?;
387 }
388 }
389 FuzzCommand::Onefuzz {
390 target,
391 config_path,
392 toolchain,
393 out_dir,
394 } => {
395 let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, target)?;
396
397 if !out_dir.exists() {
398 fs_err::create_dir_all(&out_dir)?;
399 }
400
401 let config_contents = fs_err::read_to_string(config_path)
402 .context("failed to read configuration toml")?;
403 let cfg = toml_edit::de::from_str(&config_contents)
404 .context("failed to parse onefuzz.toml")?;
405
406 for (name, target) in &selected_fuzz_targets {
407 log::info!("building '{}'", name);
408 CargoFuzzCommand::Build.invoke(
409 name,
410 &target.fuzz_dir,
411 &target.target_options,
412 toolchain.as_deref(),
413 &[],
414 )?;
415
416 log::info!("copying '{}' to output folder", name);
417 std::fs::copy(
420 format!("target/x86_64-unknown-linux-gnu/release/{}", name),
421 out_dir.join(name),
422 )?;
423
424 log::info!("emitting onefuzz allowlist for '{name}'");
425 let mut allowlist_file =
426 fs_err::File::create(out_dir.join(name).with_extension("txt"))?;
427 for path in &target.allowlist {
428 let Ok(path) = path.strip_prefix(&ctx.root) else {
429 anyhow::bail!(
432 "allowlist for '{name}' references file(s) outside of the HvLite directory"
433 )
434 };
435 writeln!(allowlist_file, "*/{}", path.display())?;
437 }
438 }
439
440 log::info!("emitting OneFuzzConfig.json");
441 let config_file =
442 fs_err::File::create(out_dir.join("OneFuzzConfig").with_extension("json"))?;
443 let config = onefuzz_schema::OneFuzzConfigV3 {
444 config_version: 3,
445 entries: selected_fuzz_targets
446 .into_iter()
447 .map(|(name, target)| make_onefuzz_entry(name, target.target_options, &cfg))
448 .collect(),
449 };
450 serde_json::to_writer(config_file, &config)?;
451 }
452 FuzzCommand::Clean {
453 targets,
454 keep_corpus,
455 keep_artifacts,
456 keep_coverage,
457 } => {
458 let selected_fuzz_targets = filter_fuzz_targets(fuzz_targets, targets)?;
459
460 for (name, meta) in selected_fuzz_targets {
461 let rm_dir = |base: &str| -> std::io::Result<()> {
462 let dir = meta.fuzz_dir.join(base);
463 let target_dir = dir.join(&name);
464 if target_dir.exists() {
465 fs_err::remove_dir_all(dir.join(&name))?;
466 }
467 if dir.exists() && fs_err::read_dir(&dir)?.count() == 0 {
468 fs_err::remove_dir(dir)?;
469 }
470
471 Ok(())
472 };
473
474 if !keep_artifacts {
475 rm_dir("artifacts")?
476 }
477
478 if !keep_corpus {
479 rm_dir("corpus")?
480 }
481
482 if !keep_coverage {
483 rm_dir("coverage")?
484 }
485 }
486 }
487 }
488
489 Ok(())
490 }
491}
492
493fn make_onefuzz_entry(
494 name: String,
495 target_options: Vec<String>,
496 cfg: &OnefuzzToml,
497) -> onefuzz_schema::Entry {
498 let my_cfg = cfg.overrides.get(&name);
499 let use_cfg = OnefuzzTomlConfig {
500 owner: my_cfg
501 .and_then(|m| m.owner.clone())
502 .unwrap_or(cfg.default.owner.clone()),
503 project_name: my_cfg
504 .and_then(|m| m.project_name.clone())
505 .unwrap_or(cfg.default.project_name.clone()),
506 ado_org: my_cfg
507 .and_then(|m| m.ado_org.clone())
508 .unwrap_or(cfg.default.ado_org.clone()),
509 ado_project: my_cfg
510 .and_then(|m| m.ado_project.clone())
511 .unwrap_or(cfg.default.ado_project.clone()),
512 ado_assigned_to: my_cfg
513 .and_then(|m| m.ado_assigned_to.clone())
514 .unwrap_or(cfg.default.ado_assigned_to.clone()),
515 ado_area_path: my_cfg
516 .and_then(|m| m.ado_area_path.clone())
517 .unwrap_or(cfg.default.ado_area_path.clone()),
518 ado_iteration_path: my_cfg
519 .and_then(|m| m.ado_iteration_path.clone())
520 .unwrap_or(cfg.default.ado_iteration_path.clone()),
521 ado_tags: my_cfg
522 .and_then(|m| m.ado_tags.clone())
523 .unwrap_or(cfg.default.ado_tags.clone()),
524 };
525
526 onefuzz_schema::Entry {
527 job_notification_email: use_cfg.owner,
528 fuzzer: onefuzz_schema::Fuzzer {
529 type_field: "libfuzzer".to_owned(),
530 fuzzing_harness_executable_name: name.clone(),
531 sources_allow_list_path: format!("{}.txt", name),
532 },
533 job_dependencies: vec![name.clone()],
534 one_fuzz_jobs: vec![onefuzz_schema::OneFuzzJob {
535 project_name: use_cfg.project_name.to_owned(),
536 target_name: name.clone(),
537 target_options,
538 }],
539 ado_template: onefuzz_schema::AdoTemplate {
540 org: use_cfg.ado_org,
541 project: use_cfg.ado_project,
542 assigned_to: use_cfg.ado_assigned_to,
543 area_path: use_cfg.ado_area_path,
544 iteration_path: use_cfg.ado_iteration_path,
545 ado_fields: onefuzz_schema::AdoFields {
546 tags: use_cfg.ado_tags,
547 },
548 },
549 }
550}
551
552#[derive(Parser)]
555pub struct VerifyFuzzers;
556
557impl Xtask for VerifyFuzzers {
558 fn run(self, ctx: crate::XtaskCtx) -> anyhow::Result<()> {
559 let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(&ctx)?;
560 let _fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
561 Ok(())
562 }
563}
564
565fn select_fuzz_target(
566 mut fuzz_targets: BTreeMap<String, RepoFuzzTarget>,
567 target_name: &str,
568) -> anyhow::Result<RepoFuzzTarget> {
569 match fuzz_targets.remove(target_name) {
570 Some(target) => Ok(target),
571 None => anyhow::bail!("invalid fuzz target '{}'", target_name),
572 }
573}
574
575fn filter_fuzz_targets(
576 mut fuzz_targets: BTreeMap<String, RepoFuzzTarget>,
577 specific_targets: Vec<String>,
578) -> anyhow::Result<BTreeMap<String, RepoFuzzTarget>> {
579 if specific_targets.is_empty() {
580 return Ok(fuzz_targets);
581 }
582 let mut targets = BTreeMap::new();
583 for target_name in specific_targets {
584 let Some(target) = fuzz_targets.remove(&target_name) else {
585 anyhow::bail!("invalid fuzz target '{}'", target_name)
586 };
587
588 targets.insert(target_name, target);
589 }
590
591 Ok(targets)
592}
593
594pub(crate) fn complete_fuzzer_targets(ctx: &crate::XtaskCtx) -> Vec<String> {
595 (|| {
596 let fuzz_crates = parse_fuzz_crate_toml::get_repo_fuzz_crates(ctx)?;
597 let fuzz_targets = parse_fuzz_crate_toml::get_repo_fuzz_targets(&fuzz_crates)?;
598 anyhow::Ok(fuzz_targets.into_keys().collect::<Vec<String>>())
599 })()
600 .unwrap_or_default()
601}
602
603#[derive(Deserialize)]
604struct OnefuzzToml {
605 default: OnefuzzTomlConfig,
606 overrides: BTreeMap<String, OnefuzzTomlOverrides>,
607}
608
609#[derive(Deserialize)]
610struct OnefuzzTomlConfig {
611 owner: String,
612 project_name: String,
613 ado_org: String,
614 ado_project: String,
615 ado_assigned_to: String,
616 ado_area_path: String,
617 ado_iteration_path: String,
618 ado_tags: String,
619}
620
621#[derive(Deserialize)]
622struct OnefuzzTomlOverrides {
623 owner: Option<String>,
624 project_name: Option<String>,
625 ado_org: Option<String>,
626 ado_project: Option<String>,
627 ado_assigned_to: Option<String>,
628 ado_area_path: Option<String>,
629 ado_iteration_path: Option<String>,
630 ado_tags: Option<String>,
631}